#
# Set of functions that will be used by our own implementation of dhclient-script.
# Most of the functions are coming from an old fedora-14 dhclient-script.
# See also usr/share/rear/lib/network-functions.sh and
# cf. https://github.com/rear/rear/issues/1517
#

# See the comments in usr/share/rear/lib/network-functions.sh
# what that values mean and how to generate them:
NETMASKS=( 0
           2147483648 3221225472 3758096384 4026531840
           4160749568 4227858432 4261412864 4278190080
           4286578688 4290772992 4292870144 4293918720
           4294443008 4294705152 4294836224 4294901760
           4294934528 4294950912 4294959104 4294963200
           4294965248 4294966272 4294966784 4294967040
           4294967168 4294967232 4294967264 4294967280
           4294967288 4294967292 4294967294 4294967295
           -1 )

exit_with_hooks() {
    exit_status="${1}"

    if [ -x ${ETCDIR}/dhclient-exit-hooks ]; then
        . ${ETCDIR}/dhclient-exit-hooks
    fi

    exit ${exit_status}
}

logmessage() {
    msg="${1}"
    logger -p ${LOGFACILITY}.${LOGLEVEL} -t "NET" "dhclient: ${msg}"
}

save_previous() {
    origfile="${1}"
    savefile="${SAVEDIR}/${origfile##*/}.predhclient.${interface}"

    if [ ! -d ${SAVEDIR} ]; then
        mkdir -p ${SAVEDIR}
    fi

    if [ -e ${origfile} ]; then
        contents="$(< ${origfile})"
        echo "${contents}" > ${savefile}
        rm -f ${origfile}
    else
        echo > ${savefile}
    fi

}

eventually_add_hostnames_domain_to_search() {
# For the case when hostname for this machine has a domain that is not in domain_search list
# 1) get the domain from this hostname
# 2) add this domain to search line in resolv.conf if it's not already
#    there (domain list that we have recently added there is a parameter of this function)
    search="${1}"
    domain=$(hostname 2>/dev/null | cut -s -d "." -f 2-)

    if [ -n "${domain}" ] &&
       [ ! "${domain}" = "localdomain" ] &&
       [ ! "${domain}" = "localdomain6" ] &&
       [ ! "${domain}" = "(none)" ] &&
       [[ ! "${domain}" = *\ * ]]; then
       is_in="false"
       for s in ${search}; do
           if [ "${s}" = "${domain}" ] ||
              [ "${s}" = "${domain}." ]; then
              is_in="true"
           fi
       done

       if [ "${is_in}" = "false" ]; then
          # Add domain name to search list (#637763)
          sed -i -e "s/${search}/${search} ${domain}/" /etc/resolv.conf
       fi
    fi
}

make_resolv_conf() {

    if [ "${reason}" = "RENEW" ] &&
       [ "${new_domain_name}" = "${old_domain_name}" ] &&
       [ "${new_domain_name_servers}" = "${old_domain_name_servers}" ]; then
        return
    fi

    if [ -n "${new_domain_name}" ] ||
       [ -n "${new_domain_name_servers}" ] ||
       [ -n "${new_domain_search}" ]; then
        save_previous /etc/resolv.conf
        rscf=$( mktemp )
        echo "; generated by /bin/dhclient-script" > ${rscf}

        if [ -n "${SEARCH}" ]; then
            search="${SEARCH}"
        else
            if [ -n "${new_domain_search}" ]; then
                # Remove instances of \032 (#450042)
                search="${new_domain_search//\\032/ }"
            elif [ -n "${new_domain_name}" ]; then
                # Note that the DHCP 'Domain Name Option' is really just a domain
                # name, and that this practice of using the domain name option as
                # a search path is both nonstandard and deprecated.
                search="${new_domain_name}"
            fi
        fi

        if [ -n "${search}" ]; then
            echo "search ${search}" >> $rscf
        fi

        if [ -n "${RES_OPTIONS}" ]; then
            echo "options ${RES_OPTIONS}" >> ${rscf}
        fi

        for nameserver in ${new_domain_name_servers} ; do
            echo "nameserver ${nameserver}" >> ${rscf}
        done

        change_resolv_conf ${rscf}
        rm -f ${rscf}

        if [ -n "${search}" ]; then
            eventually_add_hostnames_domain_to_search "${search}"
        fi

    elif [ -n "${new_dhcp6_name_servers}" ] ||
         [ -n "${new_dhcp6_domain_search}" ]; then
        save_previous /etc/resolv.conf
        rscf=$( mktemp )
        echo "; generated by /bin/dhclient-script" > ${rscf}

        if [ -n "${SEARCH}" ]; then
            search="${SEARCH}"
        else
            if [ -n "${new_dhcp6_domain_search}" ]; then
                search="${new_dhcp6_domain_search//\\032/ }"
            fi
        fi

        if [ -n "${search}" ]; then
            echo "search ${search}" >> $rscf
        fi

        if [ -n "${RES_OPTIONS}" ]; then
            echo "options ${RES_OPTIONS}" >> ${rscf}
        fi

        for nameserver in ${new_dhcp6_name_servers} ; do
            echo "nameserver ${nameserver}" >> ${rscf}
        done

        change_resolv_conf ${rscf}
        rm -f $v ${rscf} >&2

        if [ -n "${search}" ]; then
            eventually_add_hostnames_domain_to_search "${search}"
        fi

    fi
}

# Invoke this when /etc/resolv.conf has changed:
change_resolv_conf ()
{
    s=$(/bin/grep '^[\ \	]*option' /etc/resolv.conf 2>/dev/null);
    if [ "x$s" != "x" ]; then
       s="$s"$'\n';
    fi;
    if [ $# -gt 1 ]; then
       n_args=$#;
       while [ $n_args -gt 0 ];
         do
            case "$s" in *$1*)
               shift;
               n_args=$(($n_args-1));
               continue;;
            esac;
            s="$s$1";
            shift;
            if [ $# -gt 0 ]; then
                s="$s"$'\n';
            fi;
            n_args=$(($n_args-1));
         done;
    elif [ $# -eq 1 ]; then
       if [ "x$s" != "x" ]; then
          s="$s"$(/bin/grep -vF "$s" $1);
       else
          s=$(cat $1);
       fi;
    fi;
    (echo "$s" > /etc/resolv.conf;) &>/dev/null;
    r=$?
    if [ $r -eq 0 ]; then
        logger -p local7.notice -t "$PROGRAM" -i "updated /etc/resolv.conf";
    fi;
    return $r;
}

my_ipcalc() {
    declare -i BITS ADDRESS="$(ip2num "$1")"

    if [[ "$2" ]]; then
        declare -i DEC MASK="$(ip2num "$2")"
        for BITS in "${!NETMASKS[@]}" ; do
                DEC=${NETMASKS[$BITS]}
                (( MASK == DEC )) && break
        done
        (( DEC < 0 )) && Error "Main: netmask [$2] seems to be invalid."
        NETADDR=$(num2ip "$(( ADDRESS & MASK ))")
    else
        NETADDR=$(num2ip "$ADDRESS")
        BITS=32
    fi
    echo "$NETADDR/$BITS"
}

quad2num() {
    if [ $# -eq 4 ]; then
        let n="${1} << 24 | ${2} << 16 | ${3} << 8 | ${4}"
        echo "${n}"
        return 0
    else
        echo "0"
        return 1
    fi
}

ip2num() {
	IFS="."
	quad2num ${1}
}

num2ip() {
    let n="${1}"
    let o1="(n >> 24) & 0xff"
    let o2="(n >> 16) & 0xff"
    let o3="(n >> 8) & 0xff"
    let o4="n & 0xff"
    echo "${o1}.${o2}.${o3}.${o4}"
}

get_network_address() {
# get network address for the given IP address and (netmask or prefix)
    ip="${1}"
    nm="${2}"

    if [ -n "${ip}" -a -n "${nm}" ]; then
        if [[ "${nm}" = *.* ]]; then
            :
        else
            nm=`prefix2netmask ${nm}`
        fi
        my_ipcalc ${ip} ${nm} | cut -d '/' -f 2
    fi
}

prefix2netmask() {
    pf="${1}"
    echo $(num2ip "${NETMASKS[$pf]}")
}

get_prefix() {
# get prefix for the given IP address and mask
    ip="${1}"
    nm="${2}"

    if [ -n "${ip}" -a -n "${nm}" ]; then
        my_ipcalc ${ip} ${nm} | cut -d '/' -f 2
    fi
}

class_bits() {
    let ip=$(IFS='.' ip2num $1)
    let bits=32
    let mask='255'
    for ((i=0; i <= 3; i++, 'mask<<=8')); do
        let v='ip&mask'
        if [ "$v" -eq 0 ] ; then
             let bits-=8
        else
             break
        fi
    done
    echo $bits
}

is_router_reachable() {
    # handle DHCP servers that give us a router not on our subnet
    router="${1}"
    routersubnet="$(get_network_address ${router} ${new_subnet_mask})"
    mysubnet="$(get_network_address ${new_ip_address} ${new_subnet_mask})"

    if [ ! "${routersubnet}" = "${mysubnet}" ]; then
        ip -4 route add ${router}/32 dev ${interface}
        if [ $? -eq 0 ]; then
            if ping -q -c1 -w2 -I ${interface} ${router}; then
                return 0
            else
                logmessage "DHCP router ${router} is unreachable on DHCP subnet ${mysubnet} router subnet ${routersubnet}"
                ip route del ${router}/32 dev ${interface}
                return 1
            fi
        else
            logmessage "failed to create host router for unreachable router ${router} not on subnet ${mysubnet}"
            return 1
        fi
    fi

    return 0
}

add_default_gateway() {
    router="${1}"
    metric=""

    if [ $# -gt 1 ] && [ ${2} -gt 0 ]; then
        metric="metric ${2}"
    fi

    if is_router_reachable ${router} ; then
        ip -4 route replace default via ${router} dev ${interface} ${metric}
        if [ $? -ne 0 ]; then
            logmessage "failed to create default route: ${router} dev ${interface} ${metric}"
            return 1
        else
            return 0
        fi
    fi

    return 1
}

flush_dev() {
# Instead of bringing the interface down (#574568)
# explicitly clear the ARP cache and flush all addresses & routes.
    ip -4 addr flush dev ${1} &>/dev/null
    ip -4 route flush dev ${1} &>/dev/null
    ip -4 neigh flush dev ${1} &>/dev/null
}

dhconfig() {
    if [ -n "${old_ip_address}" ] && [ -n "${alias_ip_address}" ] &&
       [ ! "${alias_ip_address}" = "${old_ip_address}" ]; then
        # possible new alias, remove old alias first
        ip -4 addr del ${old_ip_address} dev ${interface}:0
    fi

    if [ -n "${old_ip_address}" ] &&
       [ ! "${old_ip_address}" = "${new_ip_address}" ]; then
        # IP address changed. Delete all routes, and clear the ARP cache.
        flush_dev ${interface}
    fi

    if [ "${reason}" = "BOUND" ] || [ "${reason}" = "REBOOT" ] ||
       [ ! "${old_ip_address}" = "${new_ip_address}" ] ||
       [ ! "${old_subnet_mask}" = "${new_subnet_mask}" ] ||
       [ ! "${old_network_number}" = "${new_network_number}" ] ||
       [ ! "${old_broadcast_address}" = "${new_broadcast_address}" ] ||
       [ ! "${old_routers}" = "${new_routers}" ] ||
       [ ! "${old_interface_mtu}" = "${new_interface_mtu}" ]; then
        ip -4 addr add ${new_ip_address}/${new_prefix} broadcast ${new_broadcast_address} dev ${interface}
        ip link set dev ${interface} up

        # The 576 MTU is only used for X.25 and dialup connections
        # where the admin wants low latency.  Such a low MTU can cause
        # problems with UDP traffic, among other things.  As such,
        # disallow MTUs from 576 and below by default, so that broken
        # MTUs are ignored, but higher stuff is allowed (1492, 1500, etc).
        if [ -n "${new_interface_mtu}" ] && [ ${new_interface_mtu} -gt 576 ]; then
            ip link set ${interface} mtu ${new_interface_mtu}
        fi

        if [ -x ${ETCDIR}/dhclient-${interface}-up-hooks ]; then
            . ${ETCDIR}/dhclient-${interface}-up-hooks
        elif [ -x ${ETCDIR}/dhclient-up-hooks ]; then
            . ${ETCDIR}/dhclient-up-hooks
        fi

        # static routes
        if [ -n "${new_classless_static_routes}" ] ||
           [ -n "${new_static_routes}" ]; then
            if [ -n "${new_classless_static_routes}" ]; then
                IFS=', |' static_routes=(${new_classless_static_routes})
            else
                IFS=', |' static_routes=(${new_static_routes})
            fi
            route_targets=()

            for((i=0; i<${#static_routes[@]}; i+=2)); do
                target=${static_routes[$i]}
                if [ -n "${new_classless_static_routes}" ]; then
                    if [ ${target} = "0" ]; then
                        # If the DHCP server returns both a Classless Static Routes option and
                        # a Router option, the DHCP client MUST ignore the Router option. (RFC3442)
                        new_routers=""
                        prefix="0"
                    else
                        prefix=$(echo ${target} | cut -d "." -f 1)
                        target=$(echo ${target} | cut -d "." -f 2-)
                        IFS="." target_arr=(${target})
                        unset IFS
                        ((pads=4-${#target_arr[@]}))
                        for j in $(seq $pads); do
                            target=${target}".0"
                        done

                        # Client MUST zero any bits in the subnet number where the corresponding bit in the mask is zero.
                        # In other words, the subnet number installed in the routing table is the logical AND of
                        # the subnet number and subnet mask given in the Classless Static Routes option. (RFC3442)
                        target="$(get_network_address ${target} ${prefix})"
                    fi
                else
                    prefix=$(class_bits ${target})
                fi
                gateway=${static_routes[$i+1]}

                metric=''
                for t in "${route_targets[@]}" ; do
                    if [ ${t} = ${target} ]; then
                        if [ -z "${metric}" ]; then
                            metric=1
                        else
                            ((metric=metric+1))
                        fi
                    fi
                done

                if [ -n "${metric}" ]; then
                    metric="metric ${metric}"
                fi

                if is_router_reachable ${gateway}; then
                    ip -4 route replace ${target}/${prefix} proto static via ${gateway} dev ${interface} ${metric}

                    if [ $? -ne 0 ]; then
                        logmessage "failed to create static route: ${target}/${prefix} via ${gateway} dev ${interface} ${metric}"
                    else
                        route_targets+=( ${target} )
                    fi
                fi
            done
        fi

        # gateways
        if [[ ( "${DEFROUTE}" != "no") &&
              (( -z "${GATEWAYDEV}" ) ||
               ( "${GATEWAYDEV}" = "${interface}" )) ]]; then
            if [[ ( -z "$GATEWAY" ) ||
                  (( -n "$DHCLIENT_IGNORE_GATEWAY" ) &&
                   ( "$DHCLIENT_IGNORE_GATEWAY" = [Yy]* )) ]]; then
                metric="${METRIC:-}"
                let i="${METRIC:-0}"
                default_routers=()

                for router in ${new_routers} ; do
                    added_router=-

                    for r in "${default_routers[@]}" ; do
                        if [ "${r}" = "${router}" ]; then
                            added_router=1
                        fi
                    done

                    if [ -z "${router}" ] ||
                       [ "${added_router}" = "1" ] ||
                       [ $(IFS=. ip2num ${router}) -le 0 ] ||
                       [[ ( "${router}" = "${new_broadcast_address}" ) &&
                          ( "${new_subnet_mask}" != "255.255.255.255" ) ]]; then
                        continue
                    fi

                    default_routers+=( ${router} )
                    add_default_gateway ${router} ${metric}
                    let i=i+1
                    metric=${i}
                done
            elif [ -n "${GATEWAY}" ]; then
                routersubnet=$(get_network_address ${GATEWAY} ${new_subnet_mask})
                mysubnet=$(get_network_address ${new_ip_address} ${new_subnet_mask})

                if [ "${routersubnet}" = "${mysubnet}" ]; then
                    ip -4 route replace default via ${GATEWAY} dev ${interface}
                fi
            fi
        fi

    fi

    if [ ! "${new_ip_address}" = "${alias_ip_address}" ] &&
       [ -n "${alias_ip_address}" ]; then
        ip -4 addr flush dev ${interface}:0 &>/dev/null
        ip -4 addr add ${alias_ip_address}/${alias_prefix} dev ${interface}:0
        ip -4 route replace ${alias_ip_address}/32 dev ${interface}:0
    fi

    make_resolv_conf

    if [ -n "${new_host_name}" ] && need_hostname; then
        hostname ${new_host_name} || echo "See -nc option in dhclient(8) man page."
    fi

    if [ -n "${DHCP_TIME_OFFSET_SETS_TIMEZONE}" ] &&
       [[ "${DHCP_TIME_OFFSET_SETS_TIMEZONE}" = [yY1]* ]]; then
        if [ -n "${new_time_offset}" ]; then
            # DHCP option "time-offset" is requested by default and should be
            # handled.  The geographical zone abbreviation cannot be determined
            # from the GMT offset, but the $ZONEINFO/Etc/GMT$offset file can be
            # used - note: this disables DST.
            ((z=new_time_offset/3600))
            ((hoursWest=$(printf '%+d' $z)))

            if (( $hoursWest < 0 )); then
                # tzdata treats negative 'hours west' as positive 'gmtoff'!
                ((hoursWest*=-1))
            fi

            #tzfile=/usr/share/zoneinfo/Etc/GMT$(printf '%+d' ${hoursWest})
            #if [ -e ${tzfile} ]; then
                #save_previous /etc/localtime
            #    cp -fp ${tzfile} /etc/localtime
            #    touch /etc/localtime
            #fi
        fi
    fi

}

# Section 18.1.8. (Receipt of Reply Messages) of RFC 3315 says:
# The client SHOULD perform duplicate address detection on each of
# the addresses in any IAs it receives in the Reply message before
# using that address for traffic.
add_ipv6_addr_with_DAD() {
            ip -6 addr add ${new_ip6_address}/${new_ip6_prefixlen} \
                dev ${interface} scope global

            # repeatedly test whether newly added address passed
            # duplicate address detection (DAD)
            for i in $(seq 5); do
                sleep 1 # give the DAD some time

                # tentative flag = DAD is still not complete or failed
                duplicate=$(ip -6 addr show dev ${interface} tentative \
                              | grep ${new_ip6_address}/${new_ip6_prefixlen})

                # if there's no tentative flag, address passed DAD
                if [ -z "${duplicate}" ]; then
                    break
                fi
            done

            # if there's still tentative flag = address didn't pass DAD =
            # = it's duplicate = remove it
            if [ -n "${duplicate}" ]; then
                ip -6 addr del ${new_ip6_address}/${new_ip6_prefixlen} dev ${interface}
                exit_with_hooks 3
            fi
}

dh6config() {
    case "${reason}" in
        BOUND6)
            if [ -z "${new_ip6_address}" ] ||
               [ -z "${new_ip6_prefixlen}" ]; then
                exit_with_hooks 2
            fi

            add_ipv6_addr_with_DAD

            make_resolv_conf
            ;;

        RENEW6|REBIND6)
            if [ -z "${new_ip6_address}" ] ||
               [ -z "${new_ip6_prefixlen}" ]; then
                exit_with_hooks 2
            fi

            if [  ! "${new_ip6_address}" = "${old_ip6_address}" ]; then
                add_ipv6_addr_with_DAD
            fi

            if [ ! "${new_dhcp6_name_servers}" = "${old_dhcp6_name_servers}" ] ||
               [ ! "${new_dhcp6_domain_search}" = "${old_dhcp6_domain_search}" ]; then
                make_resolv_conf
            fi
            ;;

        DEPREF6)
            if [ -z "${new_ip6_prefixlen}" ]; then
                exit_with_hooks 2
            fi

            ip -6 addr change ${new_ip6_address}/${new_ip6_prefixlen} \
                dev ${interface} scope global preferred_lft 0
            ;;
    esac

}

get_hwaddr ()
{
    if [ -f /sys/class/net/${1}/address ]; then
        awk '{ print toupper($0) }' < /sys/class/net/${1}/address
    elif [ -d "/sys/class/net/${1}" ]; then
        LC_ALL= LANG= ip -o link show ${1} 2>/dev/null | \
            awk '{ print toupper(gensub(/.*link\/[^ ]* ([[:alnum:]:]*).*/,
                                        "\\1", 1)); }'
    fi
}

get_device_by_hwaddr ()
{
    LANG=C ip -o link | grep -v link/ieee802.11 | grep -i "$1" | awk -F ": " '{print $2}'
}

need_hostname ()
{
    CHECK_HOSTNAME=$(hostname)
    if [ "$CHECK_HOSTNAME" = "(none)" -o "$CHECK_HOSTNAME" = "localhost" -o \
        "$CHECK_HOSTNAME" = "localhost.localdomain" ]; then
        return 0
    else
        return 1
    fi
}

set_hostname ()
{
    hostname $1
    if ! grep -q search /etc/resolv.conf; then
        domain=$(echo $1 | sed 's/^[^\.]*\.//')
        if [ -n "$domain" ]; then
                rsctmp=$( mktemp )
                cat /etc/resolv.conf > $rsctmp
                echo "search $domain" >> $rsctmp
                change_resolv_conf $rsctmp
                /bin/rm -f $rsctmp
        fi
    fi
}

check_device_down ()
{
     if LC_ALL=C ip -o link show dev $1 2>/dev/null | grep -q ",UP" ; then
        return 1
     else
        return 0
     fi
}

check_link_down ()
{
        if ! LC_ALL=C ip link show dev $1 2>/dev/null | grep -q ",UP" ; then
           ip link set dev $1 up &>/dev/null
        fi
        timeout=0
        delay=10
        [ -n "$LINKDELAY" ] && delay=$(($LINKDELAY * 2))
        while [ $timeout -le $delay ]; do
            [ "$(cat /sys/class/net/$REALDEVICE/carrier 2>/dev/null)" != "0" ] && return 1
            usleep 500000
            timeout=$((timeout+1))
        done
        return 0
}

check_default_route ()
{
    LC_ALL=C ip route list match 0.0.0.0/0 | grep -q default
}

find_gateway_dev ()
{
    if [ -n "${GATEWAY}" -a "${GATEWAY}" != "none" ] ; then
        dev=$(LC_ALL=C /bin/ip route get to "${GATEWAY}" 2>/dev/null | \
            sed -n 's/.* dev \([[:alnum:]]*\) .*/\1/p')
        if [ -n "$dev" ]; then
            GATEWAYDEV="$dev"
        fi
    fi
}

add_default_route ()
{
    check_default_route && return 0
    find_gateway_dev
    if [ "$GATEWAYDEV" != "" -a -n "${GATEWAY}" -a \
                "${GATEWAY}" != "none" ]; then
        if ! check_device_down $1; then
            if [ "$GATEWAY" = "0.0.0.0" ]; then
                /bin/ip route add default dev ${GATEWAYDEV}
            else
                /bin/ip route add default via ${GATEWAY}
            fi
        fi
    elif [ -f /etc/default-routes ]; then
        while read spec; do
            /bin/ip route add $spec
        done < /etc/default-routes
        rm -f /etc/default-routes
    fi
}

is_wireless_device ()
{
    [ -d "/sys/class/net/$1/wireless" ]
}

install_bonding_driver ()
{
   [ -d "/sys/class/net/$1" ] && return 0
   [ ! -f /sys/class/net/bonding_masters ] && ( modprobe bonding || return 1 )
   echo "+$1" > /sys/class/net/bonding_masters 2>/dev/null
   return 0
}

is_bonding_device ()
{
   [ -f "/sys/class/net/$1/bonding/slaves" ]
}

